//! Extism WebAssembly plugin wrapper
//! 
//! This module provides a wrapper around Extism for loading and executing
//! WebAssembly plugins in multiple languages.

use crate::error::{Error, Result, PluginError};
use crate::models::{plugin::PluginManifest, Value, ValueMap};
use extism::{Manifest, Plugin, Wasm};
use serde_json;
use std::collections::HashMap;
use std::path::Path;
use uuid::Uuid;

/// Extism plugin wrapper
#[derive(Debug)]
pub struct ExtismPlugin {
    /// Plugin instance
    plugin: Plugin,
    
    /// Plugin manifest
    manifest: PluginManifest,
    
    /// Plugin configuration
    config: ValueMap,
}

/// Plugin manager for Extism plugins
#[derive(Debug)]
pub struct ExtismPluginManager {
    /// Loaded plugins
    plugins: HashMap<String, ExtismPlugin>,
    
    /// Plugin search paths
    search_paths: Vec<String>,
}

impl ExtismPlugin {
    /// Create a new Extism plugin from file
    pub async fn from_file(path: &str, config: ValueMap) -> Result<Self> {
        let path_obj = Path::new(path);
        
        if !path_obj.exists() {
            return Err(Error::Plugin(PluginError::LoadingFailed {
                plugin_path: path.to_string(),
                reason: "Plugin file not found".to_string(),
            }));
        }
        
        // Create Extism manifest
        let manifest = Manifest::new([Wasm::file(path)]);
        
        // Create plugin instance
        let plugin = Plugin::new(manifest, [], true)
            .map_err(|e| Error::Plugin(PluginError::LoadingFailed {
                plugin_path: path.to_string(),
                reason: e.to_string(),
            }))?;
        
        // Create plugin manifest (metadata)
        let plugin_manifest = PluginManifest::new(
            path_obj.file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("unknown")
                .to_string(),
            "1.0.0".to_string(),
            crate::models::plugin::PluginType::Wasm,
        );
        
        Ok(Self {
            plugin,
            manifest: plugin_manifest,
            config,
        })
    }
    
    /// Create a new Extism plugin from bytes
    pub async fn from_bytes(name: String, wasm_bytes: Vec<u8>, config: ValueMap) -> Result<Self> {
        // Create Extism manifest
        let manifest = Manifest::new([Wasm::data(wasm_bytes)]);
        
        // Create plugin instance
        let plugin = Plugin::new(manifest, [], true)
            .map_err(|e| Error::Plugin(PluginError::LoadingFailed {
                plugin_path: name.clone(),
                reason: e.to_string(),
            }))?;
        
        // Create plugin manifest (metadata)
        let plugin_manifest = PluginManifest::new(
            name,
            "1.0.0".to_string(),
            crate::models::plugin::PluginType::Wasm,
        );
        
        Ok(Self {
            plugin,
            manifest: plugin_manifest,
            config,
        })
    }
    
    /// Call a plugin function
    pub async fn call_function(&mut self, function_name: &str, args: &[Value]) -> Result<Value> {
        // Serialize arguments to JSON
        let input = serde_json::to_string(args)
            .map_err(|e| Error::Plugin(PluginError::ExecutionFailed {
                function_name: function_name.to_string(),
                reason: format!("Failed to serialize arguments: {}", e),
            }))?;
        
        // Call the plugin function
        let output: String = self.plugin.call(function_name, input)
            .map_err(|e| Error::Plugin(PluginError::ExecutionFailed {
                function_name: function_name.to_string(),
                reason: e.to_string(),
            }))?;
        
        // Deserialize result from JSON
        let result: Value = serde_json::from_str(&output)
            .map_err(|e| Error::Plugin(PluginError::ExecutionFailed {
                function_name: function_name.to_string(),
                reason: format!("Failed to deserialize result: {}", e),
            }))?;
        
        Ok(result)
    }
    
    /// Check if plugin has a function
    pub fn has_function(&self, function_name: &str) -> bool {
        // TODO: Implement function existence check
        // This would require Extism to expose function metadata
        true
    }
    
    /// Get plugin manifest
    pub fn manifest(&self) -> &PluginManifest {
        &self.manifest
    }
    
    /// Get plugin configuration
    pub fn config(&self) -> &ValueMap {
        &self.config
    }
    
    /// Update plugin configuration
    pub fn update_config(&mut self, config: ValueMap) {
        self.config = config;
    }
}

impl ExtismPluginManager {
    /// Create a new plugin manager
    pub fn new() -> Self {
        Self {
            plugins: HashMap::new(),
            search_paths: vec![
                "./plugins".to_string(),
                "/usr/local/lib/ai-test-platform/plugins".to_string(),
            ],
        }
    }
    
    /// Add a search path for plugins
    pub fn add_search_path(&mut self, path: String) {
        if !self.search_paths.contains(&path) {
            self.search_paths.push(path);
        }
    }

    /// Get search paths
    pub fn search_paths(&self) -> &Vec<String> {
        &self.search_paths
    }
    
    /// Load a plugin from file
    pub async fn load_plugin(&mut self, path: &str, config: ValueMap) -> Result<String> {
        let plugin_id = Uuid::new_v4().to_string();
        
        // Try to load from absolute path first
        let plugin = if Path::new(path).is_absolute() {
            ExtismPlugin::from_file(path, config).await?
        } else {
            // Search in search paths
            let mut found_path = None;
            for search_path in &self.search_paths {
                let full_path = Path::new(search_path).join(path);
                if full_path.exists() {
                    found_path = Some(full_path);
                    break;
                }
            }
            
            match found_path {
                Some(full_path) => {
                    ExtismPlugin::from_file(&full_path.to_string_lossy(), config).await?
                }
                None => {
                    return Err(Error::Plugin(PluginError::NotFound {
                        plugin_name: path.to_string(),
                    }));
                }
            }
        };
        
        tracing::info!("Loaded plugin: {} ({})", plugin.manifest().name, plugin_id);
        self.plugins.insert(plugin_id.clone(), plugin);
        
        Ok(plugin_id)
    }
    
    /// Load a plugin from bytes
    pub async fn load_plugin_from_bytes(
        &mut self,
        name: String,
        wasm_bytes: Vec<u8>,
        config: ValueMap,
    ) -> Result<String> {
        let plugin_id = Uuid::new_v4().to_string();
        let plugin = ExtismPlugin::from_bytes(name, wasm_bytes, config).await?;
        
        tracing::info!("Loaded plugin from bytes: {} ({})", plugin.manifest().name, plugin_id);
        self.plugins.insert(plugin_id.clone(), plugin);
        
        Ok(plugin_id)
    }
    
    /// Call a plugin function
    pub async fn call_function(
        &mut self,
        plugin_id: &str,
        function_name: &str,
        args: &[Value],
    ) -> Result<Value> {
        let plugin = self.plugins.get_mut(plugin_id)
            .ok_or_else(|| Error::Plugin(PluginError::NotFound {
                plugin_name: plugin_id.to_string(),
            }))?;

        if !plugin.has_function(function_name) {
            return Err(Error::Plugin(PluginError::FunctionNotFound {
                function_name: function_name.to_string(),
            }));
        }

        plugin.call_function(function_name, args).await
    }
    
    /// Unload a plugin
    pub async fn unload_plugin(&mut self, plugin_id: &str) -> Result<()> {
        if self.plugins.remove(plugin_id).is_some() {
            tracing::info!("Unloaded plugin: {}", plugin_id);
            Ok(())
        } else {
            Err(Error::Plugin(PluginError::NotFound {
                plugin_name: plugin_id.to_string(),
            }))
        }
    }
    
    /// List all loaded plugins
    pub fn list_plugins(&self) -> Vec<&ExtismPlugin> {
        self.plugins.values().collect()
    }
    
    /// Get plugin by ID
    pub fn get_plugin(&self, plugin_id: &str) -> Option<&ExtismPlugin> {
        self.plugins.get(plugin_id)
    }
    
    /// Get plugin by name
    pub fn get_plugin_by_name(&self, name: &str) -> Option<(&String, &ExtismPlugin)> {
        self.plugins.iter()
            .find(|(_, plugin)| plugin.manifest().name == name)
    }
    
    /// Check if plugin is loaded
    pub fn is_plugin_loaded(&self, plugin_id: &str) -> bool {
        self.plugins.contains_key(plugin_id)
    }
    
    /// Get plugin count
    pub fn plugin_count(&self) -> usize {
        self.plugins.len()
    }
}

impl Default for ExtismPluginManager {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_plugin_manager_creation() {
        let manager = ExtismPluginManager::new();
        assert_eq!(manager.plugin_count(), 0);
        assert!(manager.search_paths.len() > 0);
    }

    #[test]
    fn test_search_path_management() {
        let mut manager = ExtismPluginManager::new();
        let initial_count = manager.search_paths.len();
        
        manager.add_search_path("/custom/path".to_string());
        assert_eq!(manager.search_paths.len(), initial_count + 1);
        
        // Adding the same path again should not increase count
        manager.add_search_path("/custom/path".to_string());
        assert_eq!(manager.search_paths.len(), initial_count + 1);
    }
}
